﻿// #define COMMAND_TRACE

using System;
using System.Collections;
using System.Threading;
using TI_BLE_HCI_ClientLib.BaseEntities;
using TI_BLE_HCI_ClientLib.FrameManagement;
using TI_BLE_HCI_ClientLib.HCIBaseEntities;
using TI_BLE_HCI_ClientLib.HCICommands;
using TI_BLE_HCI_ClientLib.HCIEvents;
using Utilities;

#if MF_FRAMEWORK_VERSION_V4_3 || MF_FRAMEWORK_VERSION_V4_2
using Utilities.Threading;
#endif

namespace TI_BLE_HCI_ClientLib.Controllers
{
    public delegate void HCIEventHandler(HCIEvent evt);

    public class HCIController
    {
        const int COMMAND_TIMEOUT_MSECS = 5000;

        IFrameManager _frameManager;

        Queue _commandsQueue = new Queue();

        object _commandsQueueLocker = new object();

        Thread _commandSenderThread;

        ManualResetEvent _commandsQueueWaitable = new ManualResetEvent(false);

#if MF_FRAMEWORK_VERSION_V4_3 || MF_FRAMEWORK_VERSION_V4_2
        SingleThreadPool _threadPool = new SingleThreadPool();
#endif

        public HCIController(IFrameManager frameManager)
        {
            _frameManager = frameManager;

            _frameManager.FrameReceived += _frameManager_FrameReceived;

            _commandSenderThread = new Thread(commandSenderThreadProc);
            _commandSenderThread.Priority = ThreadPriority.BelowNormal;
        }

        delegate void CompletionHandler();

        public event HCIEventHandler HCIEventReceived;
        public enum ResetTypeEnum
        {
            HardReset = 0,
            SoftReset = 1
        }

        enum StateMachineStateEnum
        {
            Idle,
            Phase1,
            Phase2,
            Phase3
        }

        public virtual void Start()
        {
            if (_commandSenderThread.ThreadState != ThreadState.Running) _commandSenderThread.Start();

            _frameManager.Start();
        }

        public virtual void Stop()
        {
            _frameManager.Stop();
        }

        public bool Reset(ResetTypeEnum resetType)
        {
            StateMachineStateEnum state = StateMachineStateEnum.Idle;

            HCIEvent_HCIEXT_SystemReset evtSystemReset = null;

            ManualResetEvent commandWaitable = new ManualResetEvent(false);
            AutoResetEvent eventWaitable = new AutoResetEvent(false);

            object locker = new object();

            HCIEventHandler handler = ((evt) =>
            {
                commandWaitable.WaitOne();

                lock (locker)
                {

                    if (evt is HCIEvent_HCIEXT_SystemReset)
                    {
                        if (state == StateMachineStateEnum.Idle)
                        {
                            evtSystemReset = evt as HCIEvent_HCIEXT_SystemReset;
                            eventWaitable.Set();
                        }

                        return;
                    }
                }
            });

            try
            {
                this.HCIEventReceived += handler;

                // INVIO COMANDO HCIEXT_SYSTEM_RESET
                this.SendCommand(
                    new HCICommand_HCIEXT_SystemReset((HCICommand_HCIEXT_SystemReset.ResetTypeEnum)resetType)
                        );

                commandWaitable.Set();

                bool ok = eventWaitable.WaitOne(COMMAND_TIMEOUT_MSECS, false);

                if (!ok) return false;

                ok = (evtSystemReset != null && evtSystemReset.Status == HCIBaseEntities.HCIVendorSpecificEventHostErrorCode.Success && evtSystemReset.CmdOpCode == (ushort)HCIVendorSpecificCommandOpcode.HCIEXT_SystemReset);

                if (!ok) return false;

                Thread.Sleep(500);

                return true;
            }
            finally
            {
#if !(MF_FRAMEWORK_VERSION_V4_3 || MF_FRAMEWORK_VERSION_V4_2)
                commandWaitable.Dispose();
                eventWaitable.Dispose();
#endif
                this.HCIEventReceived -= handler;
            }
        }

        public enum ParamIDEnum
        {
            TGAP_GEN_DISC_ADV_MIN = 0,  //!< Minimum time to remain advertising, when in Discoverable mode (mSec).  Setting this parameter to 0 turns off the timeout (default).
            TGAP_LIM_ADV_TIMEOUT = 1, //!< Maximum time to remain advertising, when in Limited Discoverable mode (mSec)
            TGAP_GEN_DISC_SCAN = 2, //!< Minimum time to perform scanning, when performing General Discovery proc (mSec)
            TGAP_LIM_DISC_SCAN = 3, //!< Minimum time to perform scanning, when performing Limited Discovery proc (mSec)
            TGAP_CONN_EST_ADV_TIMEOUT = 4, //!< Advertising timeout, when performing Connection Establishment proc (mSec)
            TGAP_CONN_PARAM_TIMEOUT = 5, //!< Link Layer connection parameter update notification timer, connection parameter update proc (mSec)
            TGAP_LIM_DISC_ADV_INT_MIN = 6, //!< Minimum advertising interval, when in limited discoverable mode (mSec)
            TGAP_LIM_DISC_ADV_INT_MAX = 7, //!< Maximum advertising interval, when in limited discoverable mode (mSec)
            TGAP_GEN_DISC_ADV_INT_MIN = 8, //!< Minimum advertising interval, when in General discoverable mode (mSec)
            TGAP_GEN_DISC_ADV_INT_MAX = 9, //!< Maximum advertising interval, when in General discoverable mode (mSec)
            TGAP_CONN_ADV_INT_MIN = 10, //!< Minimum advertising interval, when in Connectable mode (mSec)
            TGAP_CONN_ADV_INT_MAX = 11, //!< Maximum advertising interval, when in Connectable mode (mSec)
            TGAP_CONN_SCAN_INT = 12, //!< Scan interval used during Link Layer Initiating state, when in Connectable mode (mSec)
            TGAP_CONN_SCAN_WIND = 13, //!< Scan window used during Link Layer Initiating state, when in Connectable mode (mSec)
            TGAP_CONN_HIGH_SCAN_INT = 14, //!< Scan interval used during Link Layer Initiating state, when in Connectable mode, high duty scan cycle scan paramaters (mSec)
            TGAP_CONN_HIGH_SCAN_WIND = 15, //!< Scan window used during Link Layer Initiating state, when in Connectable mode, high duty scan cycle scan paramaters (mSec)
            TGAP_GEN_DISC_SCAN_INT = 16, //!< Scan interval used during Link Layer Scanning state, when in General Discovery proc (mSec)
            TGAP_GEN_DISC_SCAN_WIND = 17, //!< Scan window used during Link Layer Scanning state, when in General Discovery proc (mSec)
            TGAP_LIM_DISC_SCAN_INT = 18, //!< Scan interval used during Link Layer Scanning state, when in Limited Discovery proc (mSec)
            TGAP_LIM_DISC_SCAN_WIND = 19, //!< Scan window used during Link Layer Scanning state, when in Limited Discovery proc (mSec)
            TGAP_CONN_EST_ADV = 20, //!< Advertising interval, when using Connection Establishment proc (mSec)
            TGAP_CONN_EST_INT_MIN = 21, //!< Minimum Link Layer connection interval, when using Connection Establishment proc (mSec)
            TGAP_CONN_EST_INT_MAX = 22, //!< Maximum Link Layer connection interval, when using Connection Establishment proc (mSec)
            TGAP_CONN_EST_SCAN_INT = 23, //!< Scan interval used during Link Layer Initiating state, when using Connection Establishment proc (mSec)
            TGAP_CONN_EST_SCAN_WIND = 24, //!< Scan window used during Link Layer Initiating state, when using Connection Establishment proc (mSec)
            TGAP_CONN_EST_SUPERV_TIMEOUT = 25, //!< Link Layer connection supervision timeout, when using Connection Establishment proc (mSec)
            TGAP_CONN_EST_LATENCY = 26, //!< Link Layer connection slave latency, when using Connection Establishment proc (mSec)
            TGAP_CONN_EST_MIN_CE_LEN = 27, //!< Local informational parameter about min len of connection needed, when using Connection Establishment proc (mSec)
            TGAP_CONN_EST_MAX_CE_LEN = 28, //!< Local informational parameter about max len of connection needed, when using Connection Establishment proc (mSec)
            TGAP_PRIVATE_ADDR_INT = 29, //!< Minimum Time Interval between private (resolvable) address changes. In minutes (default 15 minutes).
            TGAP_SM_TIMEOUT = 30, //!< SM Message Timeout (milliseconds). Default 30 seconds.
            TGAP_SM_MIN_KEY_LEN = 31, //!< SM Minimum Key Length supported. Default 7.
            TGAP_SM_MAX_KEY_LEN = 32, //!< SM Maximum Key Length supported. Default 16.
            TGAP_GAP_TESTCODE = 33, //!< GAP TestCodes - puts GAP into a test mode
            TGAP_SM_TESTCODE = 34, //!< SM TestCodes - puts SM into a test mode
            TGAP_AUTH_TASK_ID = 35, //!< Task ID override for Task Authentication control
            TGAP_PARAMID_MAX = 36, //!< ID MAX-valid Parameter ID
            TGAP_GATT_TESTCODE = 100,  //!< GATT TestCodes - puts GATT into a test mode (paramValue maintained by GATT)
            TGAP_ATT_TESTCODE = 101  //!< ATT TestCodes - puts ATT into a test mode (paramValue maintained by ATT)
        }

        public bool SetGAPParameter(ParamIDEnum paramID, ushort paramValue)
        {
            HCIEvent_GAP_Command_Status evtCommandStatus = null;

            ManualResetEvent commandWaitable = new ManualResetEvent(false);
            AutoResetEvent eventWaitable = new AutoResetEvent(false);

            object locker = new object();

            HCIEventHandler handler = ((evt) =>
            {
                commandWaitable.WaitOne();

                lock (locker)
                {
                    if (evt is HCIEvent_GAP_Command_Status)
                    {
                        commandWaitable.Reset();
                        evtCommandStatus = evt as HCIEvent_GAP_Command_Status;
                        eventWaitable.Set();
                    }
                }
            });

            try
            {
                this.HCIEventReceived += handler;

                // INVIO COMANDO GAP_SET_PARAM
                this.SendCommand(new HCICommand_GAP_Set_Parameter((HCICommand_GAP_Set_Parameter.ParamIDEnum)paramID, paramValue));

                commandWaitable.Set();

                bool ok = eventWaitable.WaitOne(COMMAND_TIMEOUT_MSECS, false);

                if (!ok) return false;

                return evtCommandStatus != null && evtCommandStatus.Status == HCIVendorSpecificEventHostErrorCode.Success;
            }
            finally
            {
#if !(MF_FRAMEWORK_VERSION_V4_3 || MF_FRAMEWORK_VERSION_V4_2)
                commandWaitable.Dispose();
                eventWaitable.Dispose();
#endif
                this.HCIEventReceived -= handler;
            }
        }

        protected virtual void onHCIEventReceived(HCIEvent hciEvent)
        {
#if MF_FRAMEWORK_VERSION_V4_3 || MF_FRAMEWORK_VERSION_V4_2
            _threadPool.QueueUserWorkItem((evt) =>
            {
                raiseHCIEventReceivedEvent(evt as HCIEvent);
            },
            hciEvent);
#else
            ThreadPool.QueueUserWorkItem((evt) =>
            {
                raiseHCIEventReceivedEvent(evt as HCIEvent);
            },
            hciEvent);
#endif

            //raiseHCIEventReceivedEvent(hciEvent);
        }

        void commandSenderThreadProc()
        {
            while (true)
            {
                try
                {
                    _commandsQueueWaitable.WaitOne();

                    int queuesize = -1;

                    lock (_commandsQueueLocker)
                    {
                        queuesize = _commandsQueue.Count;
                    }

                    while (queuesize > 0)
                    {
                        CommandQueueItem commandQueueItem = null;

                        lock (_commandsQueueLocker)
                        {
                            commandQueueItem = _commandsQueue.Dequeue() as CommandQueueItem;
                        }
#if COMMAND_TRACE
                        DiagnosticLog.Trace("Sending Command " + commandQueueItem.command.GetType().ToString());
#endif
                        _frameManager.SendFrame(commandQueueItem.command);
                        commandQueueItem.completionHandler();

                        lock (_commandsQueueLocker)
                        {
                            queuesize = _commandsQueue.Count;
                        }
                    }

                    _commandsQueueWaitable.Reset();
                }
                catch (Exception ex)
                {
                    DiagnosticLog.TraceWarning("HCIController.commandSenderThreadProc : " + ex.ToString());
                }
            }
        }

        private void _frameManager_FrameReceived(BaseProtocolEntity protocolEntity)
        {
            if (protocolEntity is HCIEvent)
            {
                onHCIEventReceived(protocolEntity as HCIEvent);
            }
        }

        private void raiseHCIEventReceivedEvent(HCIEvent hciEvent)
        {
            if (HCIEventReceived != null)
            {
                try
                {
                    HCIEventReceived(hciEvent);
                }
                catch (Exception ex)
                {
                    DiagnosticLog.TraceWarning("HCIController.raiseHCIEventReceivedEvent() : " + ex.ToString());
                }
            }
        }

        class CommandQueueItem
        {
            public HCICommand command;

            public CompletionHandler completionHandler;

            public CommandQueueItem(HCICommand cmd, CompletionHandler handler) { command = cmd; completionHandler = handler; }
        }

        class EventQueueItem
        {
            public HCIEvent evt;

            public CompletionHandler completionHandler;

            public EventQueueItem(HCIEvent evt, CompletionHandler handler) { this.evt = evt; completionHandler = handler; }
        }
#if MF_FRAMEWORK_VERSION_V4_3 || MF_FRAMEWORK_VERSION_V4_2
        public virtual void SendCommand(HCICommand command)
        {
            AutoResetEvent are = new AutoResetEvent(false);

            lock (_commandsQueueLocker)
            {
                _commandsQueue.Enqueue(new CommandQueueItem(command, (() => { are.Set(); })));
            }

            _commandsQueueWaitable.Set();

            are.WaitOne();
        }

        public virtual void SendCommandAsync(HCICommand command)
        {
            AutoResetEvent are = new AutoResetEvent(false);

            lock (_commandsQueueLocker)
            {
                _commandsQueue.Enqueue(new CommandQueueItem(command, (() => { })));
            }

            _commandsQueueWaitable.Set();
        }
#else
        public virtual void SendCommand(HCICommand command)
        {
            using (AutoResetEvent are = new AutoResetEvent(false))
            {

                lock (_commandsQueueLocker)
                {
                    _commandsQueue.Enqueue(new CommandQueueItem(command, (() => { are.Set(); })));
                }

                _commandsQueueWaitable.Set();

                are.WaitOne();
            }
        }

        public virtual void SendCommandAsync(HCICommand command)
        {
            using (AutoResetEvent are = new AutoResetEvent(false))
            {
                lock (_commandsQueueLocker)
                {
                    _commandsQueue.Enqueue(new CommandQueueItem(command, (() => { })));
                }

                _commandsQueueWaitable.Set();
            }
        }
#endif
    }
}